import {
  ConcreteComponent,
  Data,
  validateComponentName,
  Component,
  ComponentInternalInstance,
  getExposeProxy,
} from "./component";
import {
  ComponentOptions,
  MergedComponentOptions,
  RuntimeCompilerOptions,
} from "./componentOptions";
import { ComponentPublicInstance } from "./componentPublicInstance";
import { Directive, validateDirectiveName } from "./directives";
import { RootRenderFunction } from "./renderer";
import { InjectionKey } from "./apiInject";
import { warn } from "./warning";
import { createVNode, cloneVNode, VNode } from "./vnode";
import { RootHydrateFunction } from "./hydration";
import { devtoolsInitApp, devtoolsUnmountApp } from "./devtools";
import { isFunction, NO, isObject } from "@vue/shared";
import { version } from ".";
import { installAppCompatProperties } from "./compat/global";
import { NormalizedPropsOptions } from "./componentProps";
import { ObjectEmitsOptions } from "./componentEmits";

export interface App<HostElement = any> {
  version: string;
  config: AppConfig;
  use(plugin: Plugin, ...options: any[]): this;
  mixin(mixin: ComponentOptions): this;
  component(name: string): Component | undefined;
  component(name: string, component: Component): this;
  directive(name: string): Directive | undefined;
  directive(name: string, directive: Directive): this;
  mount(
    rootContainer: HostElement | string,
    isHydrate?: boolean,
    isSVG?: boolean
  ): ComponentPublicInstance;
  unmount(): void;
  provide<T>(key: InjectionKey<T> | string, value: T): this;

  // internal, but we need to expose these for the server-renderer and devtools
  _uid: number;
  _component: ConcreteComponent;
  _props: Data | null;
  _container: HostElement | null;
  _context: AppContext;
  _instance: ComponentInternalInstance | null;

  /**
   * v2 compat only
   */
  filter?(name: string): Function | undefined;
  filter?(name: string, filter: Function): this;

  /**
   * @internal v3 compat only
   */
  _createRoot?(options: ComponentOptions): ComponentPublicInstance;
}

export type OptionMergeFunction = (to: unknown, from: unknown) => any;

export interface AppConfig {
  // @private
  readonly isNativeTag?: (tag: string) => boolean;

  performance: boolean;
  optionMergeStrategies: Record<string, OptionMergeFunction>;
  globalProperties: Record<string, any>;
  errorHandler?: (
    err: unknown,
    instance: ComponentPublicInstance | null,
    info: string
  ) => void;
  warnHandler?: (
    msg: string,
    instance: ComponentPublicInstance | null,
    trace: string
  ) => void;

  /**
   * Options to pass to @vue/compiler-dom.
   * Only supported in runtime compiler build.
   */
  compilerOptions: RuntimeCompilerOptions;

  /**
   * @deprecated use config.compilerOptions.isCustomElement
   */
  isCustomElement?: (tag: string) => boolean;

  /**
   * Temporary config for opt-in to unwrap injected refs.
   * TODO deprecate in 3.3
   */
  unwrapInjectedRef?: boolean;
}

export interface AppContext {
  app: App; // for devtools
  config: AppConfig;
  mixins: ComponentOptions[];
  components: Record<string, Component>;
  directives: Record<string, Directive>;
  provides: Record<string | symbol, any>;

  /**
   * Cache for merged/normalized component options
   * Each app instance has its own cache because app-level global mixins and
   * optionMergeStrategies can affect merge behavior.
   * @internal
   */
  optionsCache: WeakMap<ComponentOptions, MergedComponentOptions>;
  /**
   * Cache for normalized props options
   * @internal
   */
  propsCache: WeakMap<ConcreteComponent, NormalizedPropsOptions>;
  /**
   * Cache for normalized emits options
   * @internal
   */
  emitsCache: WeakMap<ConcreteComponent, ObjectEmitsOptions | null>;
  /**
   * HMR only
   * @internal
   */
  reload?: () => void;
  /**
   * v2 compat only
   * @internal
   */
  filters?: Record<string, Function>;
}

type PluginInstallFunction = (app: App, ...options: any[]) => any;

export type Plugin =
  | (PluginInstallFunction & { install?: PluginInstallFunction })
  | {
      install: PluginInstallFunction;
    };

export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {},
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null),
    optionsCache: new WeakMap(),
    propsCache: new WeakMap(),
    emitsCache: new WeakMap(),
  };
}

export type CreateAppFunction<HostElement> = (
  rootComponent: Component,
  rootProps?: Data | null
) => App<HostElement>;

let uid = 0;

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  // 用户创建的App实例就是这个函数生成的
  // 返回createApp方法
  return function createApp(rootComponent, rootProps = null) {
    /**
     * rootComponent: 就相当于下面例子中 createApp 接收的第一个配置对象 根组件配置对象
     *  createApp({
     *    render(){}
     *    data(){
     *      return {
     *        title:'hello'
     *      }
     *    }
     *  })
     *
     * 先将组件配置对象转成组件实例
     *  一个组件里面最重要的不是 setup 而是 render 渲染函数，但是现在有个问题是 有的配置对象并没有传递 render 它还能渲染
     * vue组件工作方式
     *  组件 => 组件实例 => render() => vnode => patch(diff算法) => dom
     */
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`);
      rootProps = null;
    }

    const context = createAppContext(); // 创建一个context 基本对象 包含mixin等等重要方法
    const installedPlugins = new Set();

    let isMounted = false; // 是否是已挂载  默认是没有挂载
    /**
     * 这个APP就是用户通过 createApp方法创建之后返回的app实例 直接返回给用户使用
     * 所以这个里面一定有我们熟知的 mount 方法
     */
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {
        return context.config;
        /**
         * isNativeTag: NO,
            performance: false,
            globalProperties: {},
            optionMergeStrategies: {},
            errorHandler: undefined,
            warnHandler: undefined,
            compilerOptions: {}
         */
      },

      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          );
        }
      },

      /**
       * 一些常见的方法 如 use
       *  在Vue2的时候也有 但是使用的时候是静态方法 如 Vue.use()这样使用
       *  Vue3上全部将这些方法改到实例上变成实例方法  vue3上几乎没有静态方法
       *  这样做的好处是 可以很好的配合 打包时的 摇树优化 用就打包进去 以便减小打包后的体积
       */
      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`);
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin);
          plugin.install(app, ...options);
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin);
          plugin(app, ...options);
        } else if (__DEV__) {
          warn(
            `A plugin must either be a function or an object with an "install" ` +
              `function.`
          );
        }
        /**
         * 还有一个重要的点 这些实例方法最终都将自身返回！！
         *  这是为什么vue3可以链式调用的原因
         *  如 app.use(router()).use(vuex()).mount('#app')
         *  可以一路的链式调用下去
         */
        return app;
      },

      // mixin混入方法
      mixin(mixin: ComponentOptions) {
        if (__FEATURE_OPTIONS_API__) {
          if (!context.mixins.includes(mixin)) {
            context.mixins.push(mixin);
          } else if (__DEV__) {
            warn(
              "Mixin has already been applied to target app" +
                (mixin.name ? `: ${mixin.name}` : "")
            );
          }
        } else if (__DEV__) {
          warn("Mixins are only available in builds supporting Options API");
        }
        return app;
      },

      component(name: string, component?: Component): any {
        if (__DEV__) {
          validateComponentName(name, context.config);
        }
        if (!component) {
          return context.components[name];
        }
        if (__DEV__ && context.components[name]) {
          warn(
            `Component "${name}" has already been registered in target app.`
          );
        }
        context.components[name] = component;
        return app;
      },

      directive(name: string, directive?: Directive) {
        if (__DEV__) {
          validateDirectiveName(name);
        }

        if (!directive) {
          return context.directives[name] as any;
        }
        if (__DEV__ && context.directives[name]) {
          warn(
            `Directive "${name}" has already been registered in target app.`
          );
        }
        context.directives[name] = directive;
        return app;
      },

      // app.mount("app"); 挂载方法
      mount(
        rootContainer: HostElement, // 接收根节点
        /**
         * TS语法 ?: 表示可有可无
         */
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        /**
         * 执行mount方法时就会创建 vnode
         */
        if (!isMounted) {
          // 创建虚拟dom -- 将跟容器 变成vnode 根的虚拟DOM
          const vnode = createVNode(
            rootComponent as ConcreteComponent, // 用户的配置 如{ setup(){}... }
            rootProps
          );
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          // 虚拟DOM上存储这个节点
          vnode.appContext = context;

          // HMR root reload
          if (__DEV__) {
            /**
             * mount挂载时会至少执行一次render函数
             */
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, isSVG);
            };
          }

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any);
          } else {
            // 根据虚拟DOM 渲染出页面
            /**
             * 而且这里的 render 只会执行一遍 因为 是在mount里的
             */
            render(vnode, rootContainer, isSVG);
          }
          // mount挂载完成 isMounted 改为 true
          isMounted = true;
          app._container = rootContainer;
          // for devtools and telemetry
          (rootContainer as any).__vue_app__ = app;

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component;
            devtoolsInitApp(app, version);
          }

          return getExposeProxy(vnode.component!) || vnode.component!.proxy;
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          );
        }
      },

      // 卸载
      unmount() {
        if (isMounted) {
          render(null, app._container);
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = null;
            devtoolsUnmountApp(app);
          }
          delete app._container.__vue_app__;
        } else if (__DEV__) {
          warn(`Cannot unmount an app that is not mounted.`);
        }
      },

      provide(key, value) {
        if (__DEV__ && (key as string | symbol) in context.provides) {
          warn(
            `App already provides property with key "${String(key)}". ` +
              `It will be overwritten with the new value.`
          );
        }
        // TypeScript doesn't allow symbols as index type
        // https://github.com/Microsoft/TypeScript/issues/24587
        context.provides[key as string] = value;

        return app;
      },

      /**
       * 看到最后都没有看到 filter 所以显而易见 在vue3中 filter方法已经是被废弃掉了
       */
    });

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render);
    }

    return app;
  };
}
